﻿using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Threading;
using System.Collections;

namespace DiagramDesigner
{
    /// <summary>
    /// Enables the selection inside of a ListBox using a seleciton rectangle.
    /// </summary>
    public sealed class ListBoxSelector
    {
        /// <summary>Identifies the IsEnabled attached property.</summary>
        public static readonly DependencyProperty EnabledProperty =
            DependencyProperty.RegisterAttached("Enabled", typeof(bool), typeof(ListBoxSelector), new UIPropertyMetadata(false, IsEnabledChangedCallback));

        // This stores the ListBoxSelector for each ListBox so we can unregister it.
        private static readonly Dictionary<ListBox, ListBoxSelector> attachedControls = new Dictionary<ListBox, ListBoxSelector>();

        private readonly ListBox listBox;
        private ScrollContentPresenter scrollContent;

        private SelectionAdorner selectionRect;
        private AutoScroller autoScroller;
        private ItemsControlSelector selector;

        private bool mouseCaptured;
        private Point start;
        private Point end;

        private ListBoxSelector(ListBox listBox)
        {
            this.listBox = listBox;
            if (this.listBox.IsLoaded)
            {
                this.Register();
            }
            else
            {
                // We need to wait for it to be loaded so we can find the
                // child controls.
                this.listBox.Loaded += this.OnListBoxLoaded;
            }
        }

        /// <summary>
        /// Gets the value of the IsEnabled attached property that indicates
        /// whether a selection rectangle can be used to select items or not.
        /// </summary>
        /// <param name="obj">Object on which to get the property.</param>
        /// <returns>
        /// true if items can be selected by a selection rectangle; otherwise, false.
        /// </returns>
        public static bool GetEnabled(DependencyObject obj)
        {
            return (bool)obj.GetValue(EnabledProperty);
        }

        /// <summary>
        /// Sets the value of the IsEnabled attached property that indicates
        /// whether a selection rectangle can be used to select items or not.
        /// </summary>
        /// <param name="obj">Object on which to set the property.</param>
        /// <param name="value">Value to set.</param>
        public static void SetEnabled(DependencyObject obj, bool value)
        {
            obj.SetValue(EnabledProperty, value);
        }

        private static void IsEnabledChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            ListBox listBox = d as ListBox;
            if (listBox != null)
            {
                if ((bool)e.NewValue)
                {
                    // If we're enabling selection by a rectangle we can assume
                    // this means we want to be able to select more than one item.
                    if (listBox.SelectionMode == SelectionMode.Single)
                    {
                        listBox.SelectionMode = SelectionMode.Extended;
                    }

                    attachedControls.Add(listBox, new ListBoxSelector(listBox));
                }
                else // Unregister the selector
                {
                    ListBoxSelector selector;
                    if (attachedControls.TryGetValue(listBox, out selector))
                    {
                        attachedControls.Remove(listBox);
                        selector.UnRegister();
                    }
                }
            }
        }        

        private bool Register()
        {
            this.scrollContent = FindChild<ScrollContentPresenter>(this.listBox);
            if (this.scrollContent != null)
            {
                this.autoScroller = new AutoScroller(this.listBox);
                this.autoScroller.OffsetChanged += this.OnOffsetChanged;

                this.selectionRect = new SelectionAdorner(this.scrollContent);
                this.scrollContent.AdornerLayer.Add(this.selectionRect);

                this.selector = new ItemsControlSelector(this.listBox);
                selector.scp = this.scrollContent;

                // The ListBox intercepts the regular MouseLeftButtonDown event
                // to do its selection processing, so we need to handle the
                // PreviewMouseLeftButtonDown. The scroll content won't receive
                // the message if we click on a blank area so use the ListBox.
                this.listBox.PreviewMouseLeftButtonDown += this.OnPreviewMouseLeftButtonDown;
                this.listBox.MouseLeftButtonUp += this.OnMouseLeftButtonUp;
                this.listBox.MouseMove += this.OnMouseMove;
            }

            // Return success if we found the ScrollContentPresenter
            return (this.scrollContent != null);
        }

        private void UnRegister()
        {
            this.StopSelection();

            // Remove all the event handlers so this instance can be reclaimed by the GC.

            this.listBox.PreviewMouseLeftButtonDown -= this.OnPreviewMouseLeftButtonDown;
            this.listBox.MouseLeftButtonUp -= this.OnMouseLeftButtonUp;
            this.listBox.MouseMove -= this.OnMouseMove;

            this.autoScroller.UnRegister();
        }

        private void OnListBoxLoaded(object sender, EventArgs e)
        {
            if (this.Register())
            {
                this.listBox.Loaded -= this.OnListBoxLoaded;
            }
        }

        private void OnOffsetChanged(object sender, OffsetChangedEventArgs e)
        {
            this.selector.Scroll(e.HorizontalChange, e.VerticalChange);
            this.UpdateSelection();
        }

        private void OnMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
        {
            if (this.mouseCaptured)
            {
                this.mouseCaptured = false;
                this.scrollContent.ReleaseMouseCapture();
                this.StopSelection();
            }
        }

        private void OnMouseMove(object sender, MouseEventArgs e)
        {
            if (this.mouseCaptured)
            {
                // Get the position relative to the content of the ScrollViewer.
                this.end = e.GetPosition(this.scrollContent);
                this.autoScroller.Update(this.end);
                this.UpdateSelection();
            }
        }

        private void OnPreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
        {
            // Check that the mouse is inside the scroll content (could be on the
            // scroll bars for example).
            Point mouse = e.GetPosition(this.scrollContent);
            if ((mouse.X >= 0) && (mouse.X < this.scrollContent.ActualWidth) &&
                (mouse.Y >= 0) && (mouse.Y < this.scrollContent.ActualHeight))
            {
                this.mouseCaptured = this.TryCaptureMouse(e);
                if (this.mouseCaptured)
                {
                    this.StartSelection(mouse);
                }
                e.Handled = true;
            }

        }
        
        private bool TryCaptureMouse(MouseButtonEventArgs e)
        {
            Point position = e.GetPosition(this.scrollContent);

            // Check if there is anything under the mouse.
            UIElement element = this.scrollContent.InputHitTest(position) as UIElement;
            if (element != null)
            {


                // The ListBox will try to capture the mouse unless something
                // else captures it.
                //if (Mouse.Captured != this.listBox)
                //{                    
                //    return false; // Something else wanted the mouse, let it keep it.
                //}

                object item = GetElementFromPoint(listBox, e.GetPosition(listBox));
                if (item != null)
                {
                    //TODO: do something (or not?)
                }
                else
                {
                    //Simulate a mouse click by sending it the MouseButtonDown
                    //event based on the data we received.
                    var args = new MouseButtonEventArgs(e.MouseDevice, e.Timestamp, MouseButton.Left, e.StylusDevice);
                    args.RoutedEvent = Mouse.MouseDownEvent;
                    args.Source = e.Source;
                    element.RaiseEvent(args);
                    return false;
                }
            }

            // Either there's nothing under the mouse or the element doesn't want the mouse.
            return this.scrollContent.CaptureMouse();
        }

        private void StopSelection()
        {
            // Hide the selection rectangle and stop the auto scrolling.
            this.selectionRect.IsEnabled = false;
            this.autoScroller.IsEnabled = false;
        }

        private void StartSelection(Point location)
        {
            // We've stolen the MouseLeftButtonDown event from the ListBox
            // so we need to manually give it focus.
            this.listBox.Focus();

            this.start = location;
            this.end = location;

            // Do we need to start a new selection?
            if (((Keyboard.Modifiers & ModifierKeys.Control) == 0) &&
                ((Keyboard.Modifiers & ModifierKeys.Shift) == 0))
            {
                // Neither the shift key or control key is pressed, so
                // clear the selection.
                this.listBox.SelectedItems.Clear();
            }

            this.selector.Reset();
            this.UpdateSelection();

            this.selectionRect.IsEnabled = true;
            this.autoScroller.IsEnabled = true;
        }

        private void UpdateSelection()
        {
            // Offset the start point based on the scroll offset.
            Point start = this.autoScroller.TranslatePoint(this.start);

            // Draw the selecion rectangle.
            // Rect can't have a negative width/height...
            double x = Math.Min(start.X, this.end.X);
            double y = Math.Min(start.Y, this.end.Y);
            double width = Math.Abs(this.end.X - start.X);
            double height = Math.Abs(this.end.Y - start.Y);
            Rect area = new Rect(x, y, width, height);
            this.selectionRect.SelectionArea = area;

            // Select the items.
            // Transform the points to be relative to the ListBox.
            Point topLeft = this.scrollContent.TranslatePoint(area.TopLeft, this.listBox);
            Point bottomRight = this.scrollContent.TranslatePoint(area.BottomRight, this.listBox);

            // And select the items.
            this.selector.UpdateSelection(new Rect(topLeft, bottomRight));
        }

        #region Static methods

        internal static object GetElementFromPoint(ListBox box, Point point)
        {
            IInputElement elementobj = box.InputHitTest(point);

            UIElement element = elementobj as UIElement;
            if (element != null)
            {
                while (true)
                {
                    if (element == box)
                    {
                        return null;
                    }
                    object item = box.ItemContainerGenerator.ItemFromContainer(element);
                    bool itemFound = !(item.Equals(DependencyProperty.UnsetValue));
                    if (itemFound)
                    {
                        return item;
                    }
                    element = (UIElement)VisualTreeHelper.GetParent(element);
                }
            }
            return null;
        }

        // Finds the nearest child of the specified type, or null if one wasn't found.
        internal static T FindChild<T>(DependencyObject reference) where T : class
        {
            // Do a breadth first search.
            var queue = new Queue<DependencyObject>();
            queue.Enqueue(reference);
            while (queue.Count > 0)
            {
                DependencyObject child = queue.Dequeue();
                T obj = child as T;
                if (obj != null)
                {
                    return obj;
                }

                // Add the children to the queue to search through later.
                for (int i = 0; i < VisualTreeHelper.GetChildrenCount(child); i++)
                {
                    queue.Enqueue(VisualTreeHelper.GetChild(child, i));
                }
            }
            return null; // Not found.
        }

        #endregion

    }
}